<?php
/**
 * 微信支付
 * @author moufer<moufer@163.com>
 * @copyright (c)2001-2009 Moufersoft
 * @website www.modoer.com
 */
!defined('IN_MUDDER') && exit('Access Denied');

class payment_wxpay extends mc_pay_payment {

    public static $name = '微信支付';
    public static $intro = '微信支付是集成在微信客户端的支付功能，用户可以通过手机完成快速的支付流程。微信支付以绑定银行卡的快捷支付为基础，向用户提供安全、快捷、高效的支付服务。网址：pay.weixin.qq.com';
    public static $platform = array('pc','mobile');
    public static $setting_keys = array('wxpay_appid', 'wxpay_mchid', 'wxpay_key', 'wxpay_appsecret');

    var $transport = 'http';    //http Or https
    var $sign_type = 'MD5';
    var $security_code  = '';

    var $notify_return = TRUE;
    var $notify_end = FALSE;

    var $out_trade_no = ''; //验证时返回的商家订单ID
    var $trade_no = ''; //验证时返回的支付宝订单ID

    var $config = null; //微信支付配置信息

    public static function create_config()
    {
        static $config = null;
        if($config) return $config;

        $config = new stdClass;

        //微信公众号身份的唯一标识。审核通过后，在微信发送的邮件中查看
        $config->appid = S('pay:wxpay_appid');
        //受理商ID，身份标识
        $config->mchid = S('pay:wxpay_mchid');
        //商户支付密钥Key。审核通过后，在微信发送的邮件中查看
        $config->key = S('pay:wxpay_key');
        //JSAPI接口中获取openid，审核后在公众平台开启开发模式后可查看
        $config->appsecret = S('pay:wxpay_appsecret');

        //获取access_token过程中的跳转uri，通过跳转将code传入jsapi支付页面
        $config->js_api_call_url = S('siteurl').'api/payment/weixin/jsapi.php';

        //证书路径,注意应该填写绝对路径
        $config->sslcert_path = MUDDER_ROOT.'api'.DS.'payment'.DS.'wexin'.DS.'apiclient_cert.pem';
        $config->sslkey_path = MUDDER_ROOT.'api'.DS.'payment'.DS.'wexin'.DS.'apiclient_cert.pem';

        //异步通知url
        $config->notify_url = S('siteurl').'api/payment/weixin/notify.php';

        //本例程通过curl使用HTTP POST方法，此处可修改其超时时间，默认为30秒
        $config->curl_timeout = 30;

        return $config;
    }

    //是否使用微信内置浏览器访问
    public static function is_weixin_browser() { 
        if ( strpos($_SERVER['HTTP_USER_AGENT'], 'MicroMessenger') !== false ) {
            return true;
        }   
        return false;
    }

    function __construct() 
    {
        parent::__construct();
        $this->config = self::create_config();
    }

    public function create_setting_from()
    {
        G('loader')->helper('form');
        $elements = array();
        $elements[] = array(
            'title' => '微信公众号ID(APPID)',
            'des' => 'appid是微信公众账号或开放平台APP的唯一标识，在公众平台申请公众账号或者在开放平台申请APP账号后，微信会自动分配对应的appid。',
            'content' => form_input('wxpay_appid', S('pay:wxpay_appid'), 'txtbox2'),
        );
        $elements[] = array(
            'title' => '微信公众号Secret(APPSECRET)',
            'des' => 'AppSecret是APPID对应的接口密码。',
            'content' => form_input('wxpay_appsecret', S('pay:wxpay_appsecret'), 'txtbox2'),
        );
        $elements[] = array(
            'title' => '商户号(MCHID)',
            'des' => '商户申请微信支付后，由微信支付分配的商户收款账号。在商家平台 pay.weixin.qq.com 可以查看到。',
            'content' => form_input('wxpay_mchid', S('pay:wxpay_mchid'), 'txtbox2'),
        );
        $elements[] = array(
            'title' => '商户支付密钥Key',
            'des' => '在商家平台 pay.weixin.qq.com 设置。',
            'content' => form_input('wxpay_key', S('pay:wxpay_key'), 'txtbox2'),
        );
        return $elements;
    }

    function platform_check()
    {
        if(is_mobile() && !self::is_weixin_browser()) {
            return $this->add_error('请通过微信客户端的公众号进入支付页面。');
        }
        return true;
    }

    function get_unid() 
    {
        if($this->out_trade_no) return $this->out_trade_no;
        return 0;
    }

    function get_payment_orderid() 
    {
        if($this->trade_no) return $this->trade_no;
        return '';
    }

    //跳转到支付平台
    function goto_pay($payid, $unid) 
    {
        if(!$pay = $this->pay->read($payid)) redirect('pay_order_empty');
        $price = $pay['price']*100;
        $title = $pay['order_name'];
        $goods = $pay['goods'] ? unserialize($pay['goods']) : array();

        //转码
        if(_G('charset') != 'utf-8') {
            $title = charset_convert($title, _G('charset'), 'utf-8');
            foreach($goods as $k=>$v) {
                $goods[$k] = charset_convert($v, _G('charset'), 'utf-8');
            }
        }

        //处理惟一订单标识 $unid
        //因为微信支付订单号是在未知付前生成的，所以当第一次支付没完成时，再次编辑订单支付，
        //会造成微信支付订单号重复的问题，因此要对$unid进行特殊处理，在返回后对$unid进行解析以便系统内部获取payid
        $unid .= 'wx'.substr($this->timestamp, -6);

        //微信内JS支付
        if($this->is_weixin_browser()) {
            $wxpay = array();
            $wxpay['payid'] = $payid;
            $wxpay['body'] = $title;
            $wxpay['out_trade_no'] = $unid;
            $wxpay['total_fee'] = $price;
            G('session')->wxpay = $wxpay;

            if(!class_exists('JsApi_pub')) {
                include_once(MUDDER_ROOT."api/payment/weixin/sdk/WxPayPubHelper.php");
            }
            //使用jsapi接口
            $jsApi = new JsApi_pub($this->config);

            //通过code获得openid 触发微信返回code码
            $url = $jsApi->createOauthUrlForCode($this->config->js_api_call_url);
            location($url);
        }
        //PC里扫码支付
        else {
            $content = $this->create_payurl($payid, $title, $price, $unid, $goods);
            if(!$content) redirect('pay_wxpay_url_empty');
            echo '<!DOCTYPE html>';
            echo '<html><head>';
            echo '<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport" >';
            echo '<meta charset="'.$this->charset.'" />';
            echo '</head>';
            echo '<body>';
            echo '<h4>正在跳转进入支付页面，请稍后...</h4>';
            echo $content;
            echo '</body></html>';
            exit();
        }
    }

    function check_payurl() 
    {
        $retcode = 0;
        if($retcode < 0) redirect($retmsg);
        return $retcode;
    }

    //生成支付链接
    function create_payurl($payid, $title, $price, $unid, $goods=array()) 
    {
        $this->gateway = S('siteurl').'api/payment/weixin/native.php?';
        //构造要请求的参数数组
        $para_form = array();
        $para_form['payid'] = $payid;
        $para_form['body'] = $title;
        $para_form['out_trade_no'] = $unid;
        $para_form['total_fee'] = $price;

        //生成表单自动提交
        $content = $this->build_request_form($para_form, 'get', '确认');

        return $content;
    }

    //交易时服务器异步验证
    function notify_check() 
    {

        include_once(MUDDER_ROOT."api/payment/weixin/sdk/WxPayPubHelper.php");
        //使用通用通知接口
        $notify = new Notify_pub(self::create_config());
        //存储微信的回调
        $xml = $GLOBALS['HTTP_RAW_POST_DATA'];  
        $notify->saveData($xml);

        //$this->_log_result("notify:\r\n【接收到的notify通知】:\n".$xml."\n");

        //验证签名，并回应微信。
        //对后台通知交互时，如果微信收到商户的应答不是成功或超时，微信认为通知失败，
        //微信会通过一定的策略（如30分钟共8次）定期重新发起通知，
        //尽可能提高通知的成功率，但微信不保证通知最终能成功。
        if($notify->checkSign() == FALSE){
            $notify->setReturnParameter("return_code","FAIL");//返回状态码
            $notify->setReturnParameter("return_msg","签名失败");//返回信息
        }else{
            $notify->setReturnParameter("return_code","SUCCESS");//设置返回码
        }
        $this->notify_msg = $notify->returnXml();
        echo $returnXml;

        $result = false;
        if($notify->checkSign() == TRUE)
        {
            if ($notify->data["return_code"] == "FAIL") {
                //此处应该更新一下订单状态，商户自行增删操作
                $this->_log_result("notify:\r\n【通信出错】:\n".$xml."\n");
            }
            elseif($notify->data["result_code"] == "FAIL"){
                //此处应该更新一下订单状态，商户自行增删操作
                $this->_log_result("notify:\r\n【业务出错】:\n".$xml."\n");
            }
            else{
                $result = true;
                G('session')->wxpay = null;
                $this->out_trade_no = $notify->data['out_trade_no'];
                if($i = strpos($this->out_trade_no, 'wx')) {
                    $this->out_trade_no = substr($this->out_trade_no, 0, $i);
                }
                $this->trade_no = $notify->data['transaction_id'];
                //此处应该更新一下订单状态，商户自行增删操作
                //$this->_log_result("notify:\r\n【支付成功】:\n".$xml."\n");
            }
        }

        return $result;
    }

    /**
     * 建立请求，以表单HTML形式构造（默认）
     * @param $para_temp 请求参数数组
     * @param $method 提交方式。两个值可选：post、get
     * @param $button_name 确认按钮显示文字
     * @return 提交表单HTML文本
     */
    function build_request_form($para_temp, $method, $button_name) 
    {
        //待请求参数数组
        $para = $this->build_request_para($para_temp);
        
        $sHtml = "<form id='wxpaysubmit' name='wxpaysubmit' action='".$this->gateway
            ."' method='".$method."'>";
        while (list ($key, $val) = each ($para)) {
            $sHtml.= "<input type='hidden' name='".$key."' value='".$val."'/>";
        }

        //submit按钮控件请不要含有name属性
        $sHtml = $sHtml."<input type='submit' value='".$button_name."'></form>";
        $sHtml = $sHtml."<script>document.forms['wxpaysubmit'].submit();</script>";

        return $sHtml;
    }

    /**
     * 生成要请求给支付宝的参数数组
     * @param $para_temp 请求前的参数数组
     * @return 要请求的参数数组
     */
    function build_request_para($para_temp) 
    {
        //除去待签名参数数组中的空值和签名参数
        $para_filter = $this->_para_filter($para_temp);

        //对待签名参数数组排序
        $para_sort = $this->_arg_sort($para_filter);

        //生成签名结果
        $mysign = $this->_create_sgin($para_sort);
        
        //签名结果与签名方式加入请求提交参数组中
        $para_sort['sign'] = $mysign;
        if($para_sort['service'] != 'alipay.wap.trade.create.direct' 
                && $para_sort['service'] != 'alipay.wap.auth.authAndExecute') {
            $para_sort['sign_type'] = strtoupper(trim($this->sign_type));
        }
        
        return $para_sort;
    }

    function _create_sgin($params) 
    {
        //生成连接
        $prestr = $this->_create_linkstring($params);
        //和MD5安全校验码组合MD5操作
        $mysgin = md5($prestr . $this->security_code);
        return $mysgin;
    }

    /**
     * 对数组排序
     * @param $para 排序前的数组
     * return 排序后的数组
     */
    function _arg_sort($para) 
    {
        ksort($para);
        reset($para);
        return $para;
    }

    /**除去数组中的空值和签名参数
     *$parameter 加密参数组
     *return 去掉空值与签名参数后的新加密参数组
     */
    function _para_filter($parameter) 
    {
        $para = array();
        foreach($parameter as $key => $val) {
            if(in_array($key, array("sign", "sign_type", "act", "api", "m", "offset", "page")) || $val == "") continue;
            else $para[$key] = $parameter[$key];
        }
        return $para;
    }

    //数组拼装成URL参数连接
    function _create_linkstring($array) 
    {
        $arg = $split = "";
        foreach($array as $key=>$val) {
            $arg .= $split . $key . "=" . $val;
            $split = "&";
        }
        return $arg;
    }

    //数组拼装成URL参数连接，参数值进行urlencode编码
    function _create_linkstring_urlencode($para) 
    {
        $arg  = "";
        while (list ($key, $val) = each ($para)) {
            $arg.=$key."=".urlencode($val)."&";
        }
        //去掉最后一个&字符
        $arg = substr($arg,0,count($arg)-2);
        //如果存在转义字符，那么去掉转义
        if(get_magic_quotes_gpc()){
            $arg = stripslashes($arg);
        }
        return $arg;
    }

    /**
     * 解析远程模拟提交后返回的信息
     * @param $str_text 要解析的字符串
     * @return 解析结果
     */
    function _parse_response($str_text) 
    {
        //以“&”字符切割字符串
        $para_split = explode('&',$str_text);
        //把切割后的字符串数组变成变量与数值组合的数组
        foreach ($para_split as $item) {
            //获得第一个=字符的位置
            $nPos = strpos($item,'=');
            //获得字符串长度
            $nLen = strlen($item);
            //获得变量名
            $key = substr($item,0,$nPos);
            //获得数值
            $value = substr($item,$nPos+1,$nLen-$nPos-1);
            //放入数组中
            $para_text[$key] = $value;
        }
        
        if( ! empty ($para_text['res_data'])) {
            //解析加密部分字符串
            /*
            if($this->alipay_config['sign_type'] == '0001') {
                $para_text['res_data'] = rsaDecrypt($para_text['res_data'], $this->alipay_config['private_key_path']);
            }
            */
            //token从res_data中解析出来（也就是说res_data中已经包含token的内容）
            $doc = new DOMDocument();
            $doc->loadXML($para_text['res_data']);
            $para_text['request_token'] = $doc->getElementsByTagName( "request_token" )->item(0)->nodeValue;
        }
        
        return $para_text;
    }

    /**
     * 获取远程服务器ATN结果,验证返回URL
     * @param $notify_id 通知校验ID
     * @return 服务器ATN结果
     * 验证结果集：
     * invalid命令参数不对 出现这个错误，请检测返回处理中partner和key是否为空 
     * true 返回正确信息
     * false 请检查防火墙或者是服务器阻止端口问题以及验证时间是否超过一分钟
     */
    function get_response($notify_id) 
    {
        $transport = strtolower(trim($this->transport));
        $partner = trim($this->config['alipay_mobile_partnerid']);
        $veryfy_url = '';
        if($transport == 'https') {
            $veryfy_url = $this->https_verify_url;
        } else {
            $veryfy_url = $this->http_verify_url;
        }
        $veryfy_url = $veryfy_url."partner=" . $partner . "&notify_id=" . $notify_id;
        $responseTxt = parent::do_get_cacert($veryfy_url, $this->cacert_path);
        
        return $responseTxt;
    }

    /**
     * 获取返回时的签名验证结果
     * @param $para_temp 通知返回来的参数数组
     * @param $sign 返回的签名结果
     * @param $isSort 是否对待签名数组排序
     * @return 签名验证结果
     */
    function get_sign_veryfy($para_temp, $sign, $isSort) 
    {
        //除去待签名参数数组中的空值和签名参数
        $para = $this->_para_filter($para_temp);
        
        //对待签名参数数组排序
        if($isSort) {
            $para = $this->_arg_sort($para);
        } else {
            $para = $this->sort_notify_para($para);
        }
        
        //把数组所有元素，按照“参数=参数值”的模式用“&”字符拼接成字符串
        $prestr = $this->_create_linkstring($para);
        
        $isSgin = $this->md5_verify($prestr, $sign, $this->security_code);

        return $isSgin;
    }

    /**
     * 异步通知时，对参数做固定排序
     * @param $para 排序前的参数组
     * @return 排序后的参数组
     */
    function sort_notify_para($para) 
    {
        $para_sort['service'] = $para['service'];
        $para_sort['v'] = $para['v'];
        $para_sort['sec_id'] = $para['sec_id'];
        $para_sort['notify_data'] = $para['notify_data'];
        return $para_sort;
    }

    /**
     * 验证签名
     * @param $prestr 需要签名的字符串
     * @param $sign 签名结果
     * @param $key 私钥
     * return 签名结果
     */
    function md5_verify($prestr, $sign, $key) {
        $prestr = $prestr . $key;
        $mysgin = md5($prestr);
        return $mysgin == $sign;
    }

}

/** end **/